/*
 * CreepBase.java
 *
 * Created on December 29, 2007, 12:32 PM
 *
 * Copyright (C) 2007-2008  Hylke van der Schaaf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, in version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package hextd.creep;

import hextd.Constants.COLOR;
import hextd.Constants.DamageType;
import hextd.creep.HexTDCreep.Size;
import hextd.wave.PathLocationWaypoint;
import hextd.wave.PathfinderWaypoints;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Double;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.usoog.tdcore.gamegrid.GameGrid;
import com.usoog.tdcore.gamegrid.ScaledCanvas;
import com.usoog.tdcore.gamestate.GameState;
import com.usoog.tdcore.player.Player;

/**
 *
 * @author hylke
 */
public abstract class CreepBase implements HexTDCreep {

	protected Size size = Size.normal;
	private String name;
	private long uniqueId = -1;
	//
	protected boolean inactive = true;
	protected boolean validTarget = false;
	protected boolean dead = false;
	protected Map<String, Long> reward;
	protected COLOR damageColor = COLOR.OTHER;
	protected COLOR damageColorAnti = COLOR.OTHER;
	//
	private Player owner;
	private Player target;
	private Point2D.Double gameLocation = new Point2D.Double();
	//
	protected ScaledCanvas canvas;
	private Point2D.Double paintLocation = new Point2D.Double();
	private Point2D.Double wobble = new Point2D.Double();
	private Point2D.Double wobbleDelta = new Point2D.Double();
	protected double paintScale, offsetX, offsetY;
	protected float strokeWidth;
	protected AffineTransform atTranslate;
	//
	private int pathId = -1;
	protected PathfinderWaypoints pathfinder;
	protected PathLocationWaypoint pathLocation;
	//
	protected double preferredDelay = 0.7f;
	protected double acceleration = 1;
	protected double speed = 50 / 1000;         // segments per tick
	protected double speedMax = 50 / 1000;      // segments per tick
	protected double speedMaxBase = 50 / 1000;  // segments per tick
	protected double speedRepairTime = 0;
	protected double power = 50 / 1000;      // power > speedMax => speed = speedMax
	protected double powerBase = 50 / 1000;  // power > speedMax => speed = speedMax
	protected double powerRepairTime = 0;
	//
	protected long health;
	protected long healthMax;
	private Shape healthLineShape;
	private Stroke healthLineStroke;
	private Color healthLineColor;
	//
	private double shieldRotationStep = 0f;
	private double shieldW;
	private double shieldH;
	private int shieldO;
	private Color shieldColor = new Color(50, 50, 255);
	private Stroke shieldStroke;
	protected boolean shielded = false;
	private boolean selected = false;
	//
	protected int cleanupTicks = 5; // After death, wait 5 ticks before cleanup.
	protected GameState gameState;
	private List<CreepChangeListener> listeners;
	//
	public static DecimalFormat form = new DecimalFormat("#,###.#");
	private boolean healthChanged = true;

	/** Creates a new instance of CreepBase */
	public CreepBase(String name) {
		this.name = name;
	}

	public void init(GameState gs, HexTDCreepData data) {
		gameState = gs;
		uniqueId = gs.getUniqueId();
		reward = data.reward;
		health = data.health;
		healthMax = data.health;
		pathfinder = data.path;
		owner = pathfinder.getOwner();
		target = pathfinder.getTarget();
		pathLocation = (PathLocationWaypoint) pathfinder.getStartingPosition();
		atTranslate = new AffineTransform();
		setSize(data.size);
	}

	@Override
	public long getId() {
		return uniqueId;
	}

	@Override
	public String getInfoString() {
		String sizeText = "";
		switch (size) {
			case large:
				sizeText = "Boss!";
				break;
			case normal:
				sizeText = "Normal";
				break;
			case small:
				sizeText = "Swarm!";
				break;
		}
		String retval = "<b>Health:</b> " + CreepBase.form.format(this.healthMax) + "<br>"
				+ "<b>Size:</b> " + sizeText + "<br>"
				+ "<b>Reward:</b><br>";

		for (Entry<String, Long> entry : reward.entrySet()) {
			retval += entry.getKey() + " : " + entry.getValue() + "<br>";
		}
		return retval;
	}

	@Override
	public String getStatusString() {
		String sizeText = "";
		switch (size) {
			case large:
				sizeText = "Boss!";
				break;
			case normal:
				sizeText = "Normal";
				break;
			case small:
				sizeText = "Swarm!";
				break;
		}
		String retval = "<b>Type:</b> " + name + "<br>"
				+ "<b>Size:</b> " + sizeText + "<br>"
				+ "<b>Health:</b> " + CreepBase.form.format(health) + "<br>"
				+ "<b>Speed:</b> " + CreepBase.form.format(speed) + "<br>";
		for (Entry<String, Long> entry : reward.entrySet()) {
			retval += entry.getKey() + " : " + entry.getValue() + "<br>";
		}
		return retval;
	}

	@Override
	public Player getOwner() {
		return owner;
	}

	@Override
	public Player getTarget() {
		return target;
	}

	@Override
	public long getHealth() {
		return this.health;
	}

	@Override
	public double getDistance() {
		return pathLocation.distanceFromEnd();
	}

	@Override
	public void setSize(Size s) {
		size = s;
		switch (size) {
			case large:
				acceleration *= 2;
				break;
			case small:
				acceleration /= 2;
				break;
		}
	}

	@Override
	public void setSelected(boolean selected) {
		this.selected = selected;
	}

	@Override
	public void remove() {
		health = 0;
		dead = true;
		gameState.removeCreep(this);
	}

	@Override
	public void damage(Player shooter, long damage) {
		if (this.isValidTarget()) {
			this.health -= damage;
			healthChanged = true;
		}
		if (this.health <= 0) {
			this.validTarget = false;
			this.dead = true;
			shooter.getResources().doReceive(reward);
			owner.minionDied(this);
		}
		this.fireStatusChangedEvent();
	}

	@Override
	public void damage(Player shooter, long damage, String[] type) {
		DamageType t = null;
		COLOR c = null;
		for (String entry : type) {
			try {
				COLOR valueOf = COLOR.valueOf(entry);
				c = valueOf;
				continue;
			} catch (IllegalArgumentException e) {
			}
			try {
				DamageType valueOf = DamageType.valueOf(entry);
				t = valueOf;
				continue;
			} catch (IllegalArgumentException e) {
			}
		}
		if (t != null && c != null) {
			doDamage(shooter, damage, t, c);
			return;
		}
		if (c != null) {
			doDamage(shooter, damage, c);
		}
		damage(shooter, damage);
	}

	@Override
	public void doDamage(Player shooter, long damage, DamageType t, COLOR c) {
		double realDamage = damage;

		switch (t) {
			case normal:
				doDamage(shooter, Math.round(damage), c);
				break;
			case speed:
				if (c == COLOR.OTHER) {
					realDamage = damage / 1000.0; // Normal damage
				} else if (c == this.damageColor) {
					realDamage = damage / 2000.0; // Half damage
				} else if (c == this.damageColorAnti) {
					realDamage = damage / 500.0; // Double damage
				}
				if (this.speedMax > this.speedMaxBase * realDamage + 0.01) {
					this.speedMax = this.speedMaxBase * realDamage;
					this.speedRepairTime = 1f;
					//System.out.println("Reducing speed to "+this.speedMax);
				} else {
					this.speedRepairTime += 1f;
					//System.out.println("Increasing speed-repairtime to "+this.speedRepairTime);
				}
				this.fireStatusChangedEvent();
				break;
			case power:
				if (c == COLOR.OTHER) {
					realDamage = damage / 1000.0; // Normal damage
				} else if (c == this.damageColor) {
					realDamage = damage / 2000.0; // Half damage
				} else if (c == this.damageColorAnti) {
					realDamage = damage / 500.0; // Double damage
				}
				if (this.power > this.powerBase * realDamage + 0.01) {
					this.power = this.powerBase * realDamage;
					this.powerRepairTime = 1f;
					//System.out.println("Reducing power to "+this.power);
				} else {
					this.powerRepairTime += 1f;
					//System.out.println("Increasing power-repairtime to "+this.powerRepairTime);
				}
				this.fireStatusChangedEvent();
				break;
			case time:
				if (c == COLOR.OTHER) {
					realDamage = damage;
				} else if (c == this.damageColorAnti) {
					realDamage = damage * 0.5f;
				} else if (c == this.damageColor) {
					realDamage = damage * 2;
				}

				pathLocation.rewind(realDamage);
				//System.out.println("Rewinding with  "+segmentDif+", "+segmentPartDif);
				this.shielded = true;
				break;
		}
	}

	@Override
	public void doDamage(Player shooter, long damage, COLOR c) {
		if (c == COLOR.OTHER) {
			this.damage(shooter, damage);
		} else if (c == this.damageColor) {
			this.damage(shooter, Math.round(damage * 2f));
		} else if (c == this.damageColorAnti) {
			this.damage(shooter, Math.round(damage * 0.25f));
		} else {
			this.damage(shooter, damage);
		}
	}

	@Override
	public Double getGameLocation(Double target) {
		if (target == null) {
			return new Point2D.Double(gameLocation.x, gameLocation.y);
		}
		target.x = gameLocation.x;
		target.y = gameLocation.y;
		return target;
	}

	@Override
	public Double getPaintLocation(Double target) {
		if (target == null) {
			return new Point2D.Double(paintLocation.x, paintLocation.y);
		}
		target.x = paintLocation.x;
		target.y = paintLocation.y;
		return target;
	}

	@Override
	public double getSpeed() {
		return this.speed;
	}

	@Override
	public double getPreferredDelay() {
		return this.preferredDelay;
	}

	@Override
	public boolean isValidTarget() {
		return ((!this.inactive) && this.validTarget && !this.dead);
	}

	@Override
	public boolean isAlive() {
		return !dead;
	}

	public boolean isExpired() {
		return dead && (cleanupTicks <= 0);
	}

	@Override
	public boolean isShielded() {
		return this.shielded;
	}

	@Override
	public void doTick(int gameTime) {
		if (this.inactive) {
			// not started yet.
			this.inactive = false;
			this.validTarget = true;
			this.doTick(gameTime);
		} else if (this.dead) {
			if (cleanupTicks <= 0) {
				gameState.removeCreep(this);
			}
			cleanupTicks--;
		} else {
			double powerRatio = Math.min(1f, this.power / this.speedMaxBase);
			double speedMaxCurrent = Math.max(this.speedMax * powerRatio, 0.05f * this.speedMaxBase);
			if (this.speed < speedMaxCurrent) {
				this.speed += this.acceleration;
				this.fireStatusChangedEvent();
			}
			if (this.speed > speedMaxCurrent) {
				this.speed = speedMaxCurrent;
				this.fireStatusChangedEvent();
			}
			// Speed and power
			if (this.powerRepairTime > 0) {
				this.powerRepairTime -= 0.0125f;
				if (this.powerRepairTime <= 0) {
					this.power = this.powerBase;
				}
			}
			if (this.speedRepairTime > 0) {
				this.speedRepairTime -= 0.34f;
				if (this.speedRepairTime <= 0) {
					this.speedMax = this.speedMaxBase;
				}
			}

			// Healthbar
			if (healthChanged) {
				float healthRatio = 1.0f * this.health / this.healthMax;
				this.healthLineShape = new Line2D.Double(-0.3 * paintScale * healthRatio, -0.3 * paintScale, 0.3 * paintScale * healthRatio, -0.3 * paintScale);
			}

			pathfinder.advance(pathLocation, speed);
			if (pathLocation.isEndReached()) {
				target.removeLives(size.livesWorth);
				pathLocation = pathfinder.getStartingPosition();
			}
			pathLocation.getGameLocation(gameLocation);

			wobbleDelta.x += (Math.random() - 0.5f) / 30f - wobbleDelta.x / 60;
			wobbleDelta.y += (Math.random() - 0.5f) / 30f - wobbleDelta.y / 60;
			wobble.x = gameLocation.x + wobbleDelta.x;
			wobble.y = gameLocation.y + wobbleDelta.y;

			GameGrid grid = gameState.getGameGrid();
			if (gameLocation.x < 0
					|| gameLocation.x > grid.getMaxGameX()
					|| gameLocation.y < 0
					|| gameLocation.y > grid.getMaxGameY()) {
				this.validTarget = false; // We're not on the board.
			} else {
				this.validTarget = true;
			}

			if (canvas != null) {
				canvas.fromGameToPaintCoordinates(wobble, paintLocation);
				atTranslate.setToIdentity();
				atTranslate.translate(paintLocation.x, paintLocation.y);
			}
		}
	}

	@Override
	public void resetScale(ScaledCanvas sc) {
		canvas = sc;
		paintScale = sc.getPaintScale();
		strokeWidth = sc.getDefaultStrokeWidth();
		offsetX = sc.getOffsetX();
		offsetY = sc.getOffsetY();
		shieldStroke = new BasicStroke(2.0f * strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
		healthLineStroke = new BasicStroke(3.0f * strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
		canvas.fromGameToPaintCoordinates(wobble, paintLocation);
		atTranslate.setToIdentity();
		atTranslate.translate(paintLocation.x, paintLocation.y);
	}

	public void paintHealthLine(Graphics2D g2) {
		if (this.health > 0 && this.healthLineShape != null) {
			g2.setStroke(this.healthLineStroke);
			g2.setColor(this.healthLineColor);
			g2.draw(this.healthLineShape);
		}
	}

	public void paintShield_bottom(Graphics2D g2) {
		this.shieldRotationStep += 0.1;
		this.shieldH = 0.4f * paintScale;
		this.shieldW = this.shieldH * (float) Math.sin(this.shieldRotationStep);
		float front = (float) Math.cos(this.shieldRotationStep);
		if ((this.shieldW < 0 && front < 0) || (this.shieldW > 0 && front > 0)) {
			this.shieldO = -1;
		} else {
			this.shieldO = 1;
		}
		this.shieldW = Math.abs(this.shieldW);
		Shape bottom = new Arc2D.Double(-this.shieldW, -this.shieldH,
				this.shieldW * 2,
				this.shieldH * 2,
				this.shieldO * 90, 180,
				Arc2D.OPEN);
		g2.setStroke(this.shieldStroke);
		g2.setColor(this.shieldColor);
		g2.draw(bottom);
	}

	public void paintShield_top(Graphics2D g2) {
		Shape top = new Arc2D.Double(-this.shieldW, -this.shieldH,
				this.shieldW * 2,
				this.shieldH * 2,
				-this.shieldO * 90, 180,
				Arc2D.OPEN);
		g2.setStroke(this.shieldStroke);
		g2.setColor(this.shieldColor);
		g2.draw(top);
	}

	public void paintSelected(Graphics2D g2) {
		if (this.selected) {
			g2.setColor(Color.LIGHT_GRAY);
			g2.setStroke(shieldStroke);
			g2.draw(new Ellipse2D.Double(paintLocation.x - 0.5f * paintScale, paintLocation.y - 0.5f * paintScale, paintScale, paintScale));
		}
	}

	protected void fireStatusChangedEvent() {
		if (this.listeners != null) {
			try {
				for (CreepChangeListener l : this.listeners) {
					l.enemySpriteStatusChanged();
				}
			} catch (ConcurrentModificationException e) {
			}
		}
	}

	@Override
	public void addStatusListener(CreepChangeListener l) {
		if (this.listeners == null) {
			this.listeners = new ArrayList<CreepChangeListener>();
		}
		this.listeners.add(l);
	}

	@Override
	public void removeStatusListener(CreepChangeListener l) {
		if (this.listeners != null) {
			this.listeners.remove(l);
		}
	}

	@Override
	public double getSpeedRepairTime() {
		return speedRepairTime;
	}

	@Override
	public double getPowerRepairTime() {
		return powerRepairTime;
	}

	@Override
	public double getSpeedMaxBase() {
		return speedMaxBase;
	}

	@Override
	public double getPowerBase() {
		return powerBase;
	}

	@Override
	public int getPathId() {
		return pathId;
	}

	public void setGameState(GameState gs) {
		this.gameState = gs;
	}
}
